'''
TODO: use str(session.key() and db.Key(encoded_str) with encoded_key in the md5
'''
import sys

import random, md5
try:
    from google.appengine.ext import db
    from google.appengine.api import users
except:
    print "No google appengine available"

import logging, yaml
from pickle import loads, dumps
#from Utils import  *

from server.Utils import *
from datetime import timedelta

DEF_SESSIONID_LEN=16
DOUBLE_CLICK_TIME=200

__author__  = "Jean Maupas <jm@avidys.com>"
__status__  = "alpha"
__version__ = "0.1"
__date__    = "2009-07-01"

class SessionCode:
    SESSION_OK=0
    SESSION_SEQUENCE=1
    SESSION_MAXLIFE=2
    SESSION_TIMEOUT=3
    SESSION_ERROR=4
    SESSION_NOT_FOUND=5
    SESSION_DOUBLE_CLICK=6
    SESSION_RELOAD=7

class Connexion():
    """ details from incoming request
    ip, browser,...
    """
    def __compare__(self):
        pass # ip=ip # ...
    pass


class Session(db.Model):
    ''' Historical and current sessions
        user 1--n Session
        user.session - Session.ID == current session
        Session.user == session owner (can be null if guest)
    '''

    #SESSION_MAXIDLETIME=900
    #SESSION_MAXLIFETIME=1800
    
    _SESSION_TIMEOUT_SECONDS=300
    _SESSION_MAXLIFE_SECONDS=3000
    _SESSION_TIMEOUT=timedelta(seconds=_SESSION_TIMEOUT_SECONDS)
    _SESSION_MAXLIFE=timedelta(seconds=_SESSION_MAXLIFE_SECONDS)
    
    ID = db.StringProperty() # internal ID starting with "SID" == key_name == Session.__key__
    user = db.StringProperty() # "UID"+user.user_id() (==key()?)
    sequence = db.IntegerProperty(default=0)

    nextID = db.StringProperty() # execution phase : not displayed; prepare the execution
    displayedID = db.StringProperty() # displayed phase: redirection from before or F5 or double-click

    startTime = db.DateTimeProperty(auto_now_add=True)
    lastUpdate = db.DateTimeProperty(auto_now=True)
    lastAccess = db.DateTimeProperty()
    timeElapsed = db.ListProperty(int)
    duration = db.ListProperty(int)

    # True means the session if terminated
    checkSequence = db.BooleanProperty (default=False) # not in sequence
    checkError = db.BooleanProperty (default=False) # other error
    timeout = db.BooleanProperty (default=False)
    maxLife = db.BooleanProperty (default=False)
    checkpoints = db.StringListProperty ()
    lastUrl= db.StringListProperty () # to redirect
    
    data=db.BlobProperty() # session data in a dict

    __DATA__={} # dictionary saved in data
    
    def ended (self):
        ''' not active property: calculated'''
        return self.checkError or self.timeout or self.maxLife

    def active (self):
        ''' not active property: calculated'''
        return self.checkError==False and self.timeout==False and self.maxLife==False
    
    def check(self, displayedID):
        """check consistency with last transaction
            normally at the start of a transaction, client asking server
            provide server, and then client with next ID to use to obtain something from that session
        """
        if displayedID != md5.new(self.ID+"_"+str(self.sequence+1)).hexdigest() : # displayedID != self.nextID or
            self.checkSequence=True
            logging.info ("Session "+self.ID+" Error nextID "+displayedID+" != "+self.nextID)
            self.end()
            return False
        if not self.checkTimeout(): return False
        self.sequence += 1
        self.displayedID=self.nextID
        self.nextID=md5.new(self.ID+"_"+str(self.sequence+1)).hexdigest()
        logging.debug ("Delta="+str(datetime.utcnow()-self.lastAccess))
        self.timeElapsed=getDT(datetime.utcnow()-self.lastAccess)
        self.checkpoints.append(str(self.lastAccess))
        self.lastAccess=datetime.utcnow()
        self.put()
        return True

    def checkTimeout(self):
        """just check timeout"""
        timeout=datetime.utcnow() - self.lastAccess
        life=datetime.utcnow() - self.startTime
        if timeout > Session._SESSION_TIMEOUT:
            self.timeout=True
            self.end()
            logging.info("Session "+self.ID+" Timeout "+str(timeout))
            return False
        if life > Session._SESSION_MAXLIFE:
            self.maxLife=True
            self.end()
            logging.info ("Session "+self.ID+" maxLife "+str(life))
            return False
        self.put()
        return True

    def end(self):
        self.endTime=datetime.utcnow()
        self.duration=getDT(self.endTime-self.startTime)
        self.put()

    def update(self):
        '''update data and session time'''
        self.data=dumps(self.__DATA__)
        self.put()
         
    def set(self,name,item):
        self.__DATA__[name]=item
        self.update()

    def get(self,name):
        if name in self.__DATA__:
            return self.__DATA__[name]
        else:
            return None

    def allSessions(user=None):
        """return all sessions"""
        if user == None:
            q = db.GqlQuery("SELECT * FROM Session ORDER BY lastAccess")
        else:
            q = db.GqlQuery("SELECT * FROM Session WHERE user=:l","UID"+user)
        if q.count() == 0 : return []
        else: return q.fetch(1000) # return (str(s) for s in q)
    allSessions = staticmethod(allSessions)
   
    def render(self,t=None):
        if t=="title": return self.__class__.__name__
        if t=="header": 
            return ["ID","User","Sequence","Displayed","Next","Start Time","Last Access"]
        return [\
            str(self.ID),
            str(self.user),
            str(self.sequence),
            str(self.displayedID),
            str(self.nextID),
            str(self.startTime),
            str(self.lastAccess)
              ]
    #def __str__ (self):
    #    return "ID=%s, seq=%d, nextID=%s, display=%s, start=%s, lastUpd=%s, %s" %\
    #        (self.ID,
    #         self.sequence,
    #         self.nextID,
    #         self.displayedID,
    #         str(self.startTime),
    #         str(self.lastUpdate),
    #         str(self.checkpoints)
    #        )
    def __str__(self):
        return str([s for s in self.render()])
    

class SessionManagement(object):
    """   Session management for incoming connexions (server side)
        Inclused Session and UserPrefs - rely on external user management (google)

          Usage : 
                id=SessionManagement.startSession(user)  # id is the sid to display
                newID=SessionManagement.checkSession(id,user) # id is the id gotten from the url, newid is the new sid to display
                SessionManagement.endSession(newID)

          Factory model allows the server to know every ongoing session
          Factory : the sessionFactoryobject retain all session in DB

          Session.startSession() : create sessionFactory if not existing, generate a session, stores it
          Session.checkSession(id) : check timeouts

          TODO : Current, last : each transaction has a different session ID
          next session ID is provide to the client: sequence and timestamp
          timestamp in session hash allow to void session stealing

    """
    
    def reStartSession(self,session,user):
        '''assign a user to a session'''
        pass

    def startSession(user=None):
        '''create a new Session object and return the ID'''
        from Model import UserPrefs
        s=SessionManagement.generateKey()
        s.displayedID = md5.new(s.ID+"_"+str(0)).hexdigest()
        s.nextID = md5.new(s.ID+"_"+str(1)).hexdigest()
        s.lastAccess=datetime.utcnow()
        logging.debug("Session created="+str(s))
        if user != None:
            uid = "UID"+user
            # except: logging.critical("invalid user provided to startSession")
            gup = UserPrefs.gql('WHERE user=:1',uid)
            if gup.count() == 1:
                up=gup.get()
                up.session=s.key()
                up.put()
                s.user=uid
            elif gup.count() == 0:
                #logging.info(ERRORS[1001][2],uid)
                up=UserPrefs(key_name=uid,user=uid,lastConnexionTime= datetime.utcnow()) 
                up.put()
                s.user=uid
            else:
                logging.error("error: more than 1 UserPref for user=%s",uid)
        s.put()
        __CURRENT_SESSION__=s
        return s
    startSession = staticmethod(startSession)

    def findSession(id):
        """id is id passed to client, not ID; ID is primary ID for table and a session is easilly found in __ALL__ by ID
        """
        s = Session.gql("WHERE nextID=:1",id).get()
        if s is None:
            s = Session.gql("WHERE displayedID=:1",id).get()
            if s is None: 
                return None,None
        out=0
        if s.displayedID==id: 
            out=1
        if (datetime.utcnow()-s.lastAccess) < timedelta(seconds=0,milliseconds=DOUBLE_CLICK_TIME):
            logging.info("double click time delta:"+str((datetime.utcnow()-s.lastAccess)))
            out=2

        # same check less than 500 ms ago
        if s.data: s.__DATA__=loads(s.data)
        return out,s
    findSession = staticmethod(findSession)

    def checkSession(id):
        '''main function for non starting page'''
        out,s = SessionManagement.findSession(id)
        if s != None: 
            logging.info("session check: 0=next 1=reload 2=double-click ["+str(out)+"]")
            if out > 0: # s == False: # no check
                __CURRENT_SESSION__=s
                if out == 2: return s,SessionCode.SESSION_DOUBLE_CLICK
                else: return s,SessionCode.SESSION_RELOAD
            if s.check(id): 
                __CURRENT_SESSION__=s
                return s,SessionCode.SESSION_OK
            else:
                if s.checkSequence: return s,SessionCode.SESSION_SEQUENCE
                if s.maxLife: return s,SessionCode.SESSION_MAXLIFE
                if s.timeout: return s,SessionCode.SESSION_TIMEOUT
                return s,SessionCode.SESSION_ERROR
        return None,SessionCode.SESSION_NOT_FOUND
    checkSession = staticmethod(checkSession)

    def getUserSession(user):
        ''' retrieve the session associated with the user
            user always exists
        '''
        from Model import UserPrefs
        uid = "UID"+user
        #except: logging.error("invalid user provided to getUserSession")
            
        logging.debug ("uid from user="+uid)
        gup = UserPrefs.gql('WHERE user=:1',uid)
        if gup.count()==0: # user exists: create prefs
            logging.info ("UserPrefs created for user=%s ",uid)
            up=UserPrefs(key_name=uid,user=uid,lastConnexionTime= datetime.utcnow()) 
            up.put()
            return None,up
        if gup.count()>1: # user exists: create prefs
            logging.error("error: more than 1 UserPref for user=%s",uid)
            return None,None
        up=gup.get()
        try:
            # get session from UserPref
            logging.debug("UserPref Session=%s",up.session)
            se = Session.gql("WHERE ID=:1",up.session.ID)
            if se.count() == 0 :
                logging.error("UserPrefs.session not found in Session")
                up.session = None
                up.put() 
                return None,up
            else: 
                se=se.get()
        except : # session not ref. ReferenceProperty error
            up.session=None
            up.lastConnexionTime= datetime.utcnow()
            up.put()
            return None,up
        up.lastConnexionTime= datetime.utcnow()
        up.put()
        #se.check()
        #se.put()
        return se,up
    getUserSession = staticmethod(getUserSession)

    def gc():
        for s in Session.query.all():
            print s
            print "gc ####",s.ID,s.startTime,s.lastAccess,datetime.utcnow()-s.lastAccess,datetime.utcnow()-s.startTime
            if s.checkTimeout(): print "Session",s.ID,"ok"
    gc = staticmethod(gc)

    @staticmethod
    def saveCurrentSession(session=None):
        logging.debug ( "save currentsession" )
        if session: session.update()
        if __CURRENT_SESSION__: __CURRENT_SESSION__.update()
        
    def generateSessionID(len=DEF_SESSIONID_LEN,maxl=None):
        if maxl!=None:
            if maxl>len:
                len=random.randint(len,maxl)
            else:
                len=random.randint(maxl,len)
        return "SID"+"".join([chr(random.randint(65,90)) for i in range(len)])
    generateSessionID = staticmethod(generateSessionID)

    def generateKey():
        """generate a ID not already generated: ensure transaction"""
        while True:
            ID = SessionManagement.generateSessionID()
            s = db.run_in_transaction(SessionManagement.checkSessionID,ID)
            logging.debug("try "+ID)
            if s != None: break
        return s
    generateKey = staticmethod(generateKey)
        
    def checkSessionID(ID):
        '''
        try to create a new session if it doesnt exist with the ID
        '''
        q=Session.get_by_key_name(ID)
        #k=db.Key.from_path("Session","ID",ID)
        #q = Session.get(k)
        #q = Sessiongql("WHERE ID=:1",ID)
        logging.debug("try "+ID+" "+str(q))
        if q!=None: return None
        s = Session(key_name=ID,ID=ID)
        s.put()
        return s 
    checkSessionID = staticmethod(checkSessionID)

    def retrieveUser(user):
        q = db.GqlQuery("SELECT * FROM Session WHERE user=:1","UID"+user)
        s=q.get()
        return s
    retrieveUser = staticmethod(retrieveUser)

        
  

def allSessionsToHtml(q):
        s="<table border=1 cellspacing=0 cellpadding=2 >"
        for se in q:
            s += "<tr>"
            s += "<td>"+str(se.ID)+"</td>"
            s += "<td>"+str(se.user)+"</td>"
            s += "<td>"+str(se.sequence)+"</td>"
            s += "<td>"+str(se.displayedID)+"</td>"
            s += "<td>"+str(se.nextID)+"</td>"
            s += "<td>"+str(se.startTime)+"</td>"
            s += "<td>"+str(se.lastAccess)+"</td>"
            s += "</tr>"
        s += "</table>"
        return s
    
def saveAtExit():
    SessionManagement.saveCurrentSession()
    
__CURRENT_SESSION__=None
#Let's try and shutdown automatically on application exit...
try:
    import atexit
    atexit.register(saveAtExit)
    logging.debug("atexit registered")
except ImportError: # for Python versions < 2.0
    def exithook(status, old_exit=sys.exit):
        try:
            saveAtExit()
        finally:
            old_exit(status)

    sys.exit = exithook

if __name__ == '__main__':
    #print Session.generateSessionID()

    id=SessionManagement.startSession("test")
    print 1,id
    newID,code=SessionManagement.checkSession(id,"test")
    print 2,newID
    newID,code=SessionManagement.checkSession(newID,"test")
    print 3,newID
    newID,code=SessionManagement.checkSession(newID,"test")
    print 4,newID

    print   
    for s in Session.query.all():
        print s  
    
